// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.io.*;
import java.util.*;

/**
 * A DNS master file parser. This incrementally parses the file, returning one
 * record at a time. When directives are seen, they are added to the state and
 * used when parsing future records.
 * 
 * @author Brian Wellington
 */

public class Master {

	private Name origin;
	private File file;
	private Record last = null;
	private long defaultTTL;
	private Master included = null;
	private Tokenizer st;
	private int currentType;
	private int currentDClass;
	private long currentTTL;
	private boolean needSOATTL;

	private Generator generator;
	private List generators;
	private boolean noExpandGenerate;

	Master(File file, Name origin, long initialTTL) throws IOException {
		if (origin != null && !origin.isAbsolute()) {
			throw new RelativeNameException(origin);
		}
		this.file = file;
		st = new Tokenizer(file);
		this.origin = origin;
		defaultTTL = initialTTL;
	}

	/**
	 * Initializes the master file reader and opens the specified master file.
	 * 
	 * @param filename
	 *            The master file.
	 * @param origin
	 *            The initial origin to append to relative names.
	 * @param ttl
	 *            The initial default TTL.
	 * @throws IOException
	 *             The master file could not be opened.
	 */
	public Master(String filename, Name origin, long ttl) throws IOException {
		this(new File(filename), origin, ttl);
	}

	/**
	 * Initializes the master file reader and opens the specified master file.
	 * 
	 * @param filename
	 *            The master file.
	 * @param origin
	 *            The initial origin to append to relative names.
	 * @throws IOException
	 *             The master file could not be opened.
	 */
	public Master(String filename, Name origin) throws IOException {
		this(new File(filename), origin, -1);
	}

	/**
	 * Initializes the master file reader and opens the specified master file.
	 * 
	 * @param filename
	 *            The master file.
	 * @throws IOException
	 *             The master file could not be opened.
	 */
	public Master(String filename) throws IOException {
		this(new File(filename), null, -1);
	}

	/**
	 * Initializes the master file reader.
	 * 
	 * @param in
	 *            The input stream containing a master file.
	 * @param origin
	 *            The initial origin to append to relative names.
	 * @param ttl
	 *            The initial default TTL.
	 */
	public Master(InputStream in, Name origin, long ttl) {
		if (origin != null && !origin.isAbsolute()) {
			throw new RelativeNameException(origin);
		}
		st = new Tokenizer(in);
		this.origin = origin;
		defaultTTL = ttl;
	}

	/**
	 * Initializes the master file reader.
	 * 
	 * @param in
	 *            The input stream containing a master file.
	 * @param origin
	 *            The initial origin to append to relative names.
	 */
	public Master(InputStream in, Name origin) {
		this(in, origin, -1);
	}

	/**
	 * Initializes the master file reader.
	 * 
	 * @param in
	 *            The input stream containing a master file.
	 */
	public Master(InputStream in) {
		this(in, null, -1);
	}

	private Name parseName(String s, Name origin) throws TextParseException {
		try {
			return Name.fromString(s, origin);
		} catch (TextParseException e) {
			throw st.exception(e.getMessage());
		}
	}

	private void parseTTLClassAndType() throws IOException {
		String s;
		boolean seen_class = false;

		// This is a bit messy, since any of the following are legal:
		// class ttl type
		// ttl class type
		// class type
		// ttl type
		// type
		seen_class = false;
		s = st.getString();
		if ((currentDClass = DClass.value(s)) >= 0) {
			s = st.getString();
			seen_class = true;
		}

		currentTTL = -1;
		try {
			currentTTL = TTL.parseTTL(s);
			s = st.getString();
		} catch (NumberFormatException e) {
			if (defaultTTL >= 0)
				currentTTL = defaultTTL;
			else if (last != null)
				currentTTL = last.getTTL();
		}

		if (!seen_class) {
			if ((currentDClass = DClass.value(s)) >= 0) {
				s = st.getString();
			} else {
				currentDClass = DClass.IN;
			}
		}

		if ((currentType = Type.value(s)) < 0)
			throw st.exception("Invalid type '" + s + "'");

		// BIND allows a missing TTL for the initial SOA record, and uses
		// the SOA minimum value. If the SOA is not the first record,
		// this is an error.
		if (currentTTL < 0) {
			if (currentType != Type.SOA)
				throw st.exception("missing TTL");
			needSOATTL = true;
			currentTTL = 0;
		}
	}

	private long parseUInt32(String s) {
		if (!Character.isDigit(s.charAt(0)))
			return -1;
		try {
			long l = Long.parseLong(s);
			if (l < 0 || l > 0xFFFFFFFFL)
				return -1;
			return l;
		} catch (NumberFormatException e) {
			return -1;
		}
	}

	private void startGenerate() throws IOException {
		String s;
		int n;

		// The first field is of the form start-end[/step]
		// Regexes would be useful here.
		s = st.getIdentifier();
		n = s.indexOf("-");
		if (n < 0)
			throw st.exception("Invalid $GENERATE range specifier: " + s);
		String startstr = s.substring(0, n);
		String endstr = s.substring(n + 1);
		String stepstr = null;
		n = endstr.indexOf("/");
		if (n >= 0) {
			stepstr = endstr.substring(n + 1);
			endstr = endstr.substring(0, n);
		}
		long start = parseUInt32(startstr);
		long end = parseUInt32(endstr);
		long step;
		if (stepstr != null)
			step = parseUInt32(stepstr);
		else
			step = 1;
		if (start < 0 || end < 0 || start > end || step <= 0)
			throw st.exception("Invalid $GENERATE range specifier: " + s);

		// The next field is the name specification.
		String nameSpec = st.getIdentifier();

		// Then the ttl/class/type, in the same form as a normal record.
		// Only some types are supported.
		parseTTLClassAndType();
		if (!Generator.supportedType(currentType))
			throw st.exception("$GENERATE does not support "
					+ Type.string(currentType) + " records");

		// Next comes the rdata specification.
		String rdataSpec = st.getIdentifier();

		// That should be the end. However, we don't want to move past the
		// line yet, so put back the EOL after reading it.
		st.getEOL();
		st.unget();

		generator = new Generator(start, end, step, nameSpec, currentType,
				currentDClass, currentTTL, rdataSpec, origin);
		if (generators == null)
			generators = new ArrayList(1);
		generators.add(generator);
	}

	private void endGenerate() throws IOException {
		// Read the EOL that we put back before.
		st.getEOL();

		generator = null;
	}

	private Record nextGenerated() throws IOException {
		try {
			return generator.nextRecord();
		} catch (Tokenizer.TokenizerException e) {
			throw st.exception("Parsing $GENERATE: " + e.getBaseMessage());
		} catch (TextParseException e) {
			throw st.exception("Parsing $GENERATE: " + e.getMessage());
		}
	}

	/**
	 * Returns the next record in the master file. This will process any
	 * directives before the next record.
	 * 
	 * @return The next record.
	 * @throws IOException
	 *             The master file could not be read, or was syntactically
	 *             invalid.
	 */
	public Record _nextRecord() throws IOException {
		Tokenizer.Token token;
		String s;

		if (included != null) {
			Record rec = included.nextRecord();
			if (rec != null)
				return rec;
			included = null;
		}
		if (generator != null) {
			Record rec = nextGenerated();
			if (rec != null)
				return rec;
			endGenerate();
		}
		while (true) {
			Name name;

			token = st.get(true, false);
			if (token.type == Tokenizer.WHITESPACE) {
				Tokenizer.Token next = st.get();
				if (next.type == Tokenizer.EOL)
					continue;
				else if (next.type == Tokenizer.EOF)
					return null;
				else
					st.unget();
				if (last == null)
					throw st.exception("no owner");
				name = last.getName();
			} else if (token.type == Tokenizer.EOL)
				continue;
			else if (token.type == Tokenizer.EOF)
				return null;
			else if (((String) token.value).charAt(0) == '$') {
				s = token.value;

				if (s.equalsIgnoreCase("$ORIGIN")) {
					origin = st.getName(Name.root);
					st.getEOL();
					continue;
				} else if (s.equalsIgnoreCase("$TTL")) {
					defaultTTL = st.getTTL();
					st.getEOL();
					continue;
				} else if (s.equalsIgnoreCase("$INCLUDE")) {
					String filename = st.getString();
					File newfile;
					if (file != null) {
						String parent = file.getParent();
						newfile = new File(parent, filename);
					} else {
						newfile = new File(filename);
					}
					Name incorigin = origin;
					token = st.get();
					if (token.isString()) {
						incorigin = parseName(token.value, Name.root);
						st.getEOL();
					}
					included = new Master(newfile, incorigin, defaultTTL);
					/*
					 * If we continued, we wouldn't be looking in the new file.
					 * Recursing works better.
					 */
					return nextRecord();
				} else if (s.equalsIgnoreCase("$GENERATE")) {
					if (generator != null)
						throw new IllegalStateException("cannot nest $GENERATE");
					startGenerate();
					if (noExpandGenerate) {
						endGenerate();
						continue;
					}
					return nextGenerated();
				} else {
					throw st.exception("Invalid directive: " + s);
				}
			} else {
				s = token.value;
				name = parseName(s, origin);
				if (last != null && name.equals(last.getName())) {
					name = last.getName();
				}
			}

			parseTTLClassAndType();
			last = Record.fromString(name, currentType, currentDClass,
					currentTTL, st, origin);
			if (needSOATTL) {
				long ttl = ((SOARecord) last).getMinimum();
				last.setTTL(ttl);
				defaultTTL = ttl;
				needSOATTL = false;
			}
			return last;
		}
	}

	/**
	 * Returns the next record in the master file. This will process any
	 * directives before the next record.
	 * 
	 * @return The next record.
	 * @throws IOException
	 *             The master file could not be read, or was syntactically
	 *             invalid.
	 */
	public Record nextRecord() throws IOException {
		Record rec = null;
		try {
			rec = _nextRecord();
		} finally {
			if (rec == null) {
				st.close();
			}
		}
		return rec;
	}

	/**
	 * Specifies whether $GENERATE statements should be expanded. Whether
	 * expanded or not, the specifications for generated records are available
	 * by calling {@link #generators}. This must be called before a $GENERATE
	 * statement is seen during iteration to have an effect.
	 */
	public void expandGenerate(boolean wantExpand) {
		noExpandGenerate = !wantExpand;
	}

	/**
	 * Returns an iterator over the generators specified in the master file;
	 * that is, the parsed contents of $GENERATE statements.
	 * 
	 * @see Generator
	 */
	public Iterator generators() {
		if (generators != null)
			return Collections.unmodifiableList(generators).iterator();
		else
			return Collections.EMPTY_LIST.iterator();
	}

	protected void finalize() {
		st.close();
	}

}
